package net.cathackers.devel.scmp.auth.backends;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.management.timer.Timer;

import org.apache.log4j.Logger;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;

import net.cathackers.devel.exceptions.CATException;
import net.cathackers.devel.scmp.auth.exceptions.AuthenticationException;
import net.cathackers.devel.scmp.auth.exceptions.LockedUserException;
import net.cathackers.devel.scmp.auth.exceptions.ProxyException;
import net.cathackers.devel.scmp.auth.exceptions.UserNotFoundException;
import net.cathackers.devel.scmp.auth.impl.UniqueNumberGenerator;
import net.cathackers.devel.scmp.auth.interfaces.AuthToken;
import net.cathackers.devel.scmp.auth.interfaces.AuthenticationCredentials;
import net.cathackers.devel.scmp.auth.interfaces.AuthenticationInformation;
import net.cathackers.devel.scmp.auth.interfaces.Authenticator;
import net.cathackers.devel.scmp.auth.interfaces.UserID;
import net.cathackers.devel.scmp.config.impl.XMLLDAPConfig;
import net.cathackers.devel.scmp.exceptions.BackendConnectException;
import net.cathackers.devel.scmp.exceptions.BackendException;
import net.cathackers.devel.scmp.factories.SCMPFactory;

public class LDAPAuthenticatorBackend implements Authenticator {
    private Map<AuthToken, UserID> tokensCache_;
    private Logger logger_;
    private XMLLDAPConfig ldapConfig_; 
    private LDAPConnection connection_;
    private UniqueNumberGenerator randomGenerator_;
    
    public LDAPAuthenticatorBackend(XMLLDAPConfig ldapConfig) {
	randomGenerator_ = new UniqueNumberGenerator();
	tokensCache_ = new HashMap<AuthToken, UserID>();
	ldapConfig_ = ldapConfig;
	logger_ = SCMPFactory.getLogger(this);
	connection_ = new LDAPConnection();
    }
    
    @Override
    public void connect() throws BackendException, ProxyException {
	connect_(connection_);
	try {
	    connection_.bind(LDAPConnection.LDAP_V3, ldapConfig_.getBindDN(), ldapConfig_.getBindPassword().getBytes());
	} catch(LDAPException e) {
	    logger_.error("Couldn't bind to LDAP Server...", e);
	    throw new BackendConnectException();	    
	}
    }

    private void connect_(LDAPConnection conn) throws BackendException, ProxyException {
	try {
	    connection_.connect(ldapConfig_.getHostname(), ldapConfig_.getPort());
	} catch (LDAPException e) {
	    logger_.error("Couldn't connect to LDAP server", e);
	    throw new BackendConnectException();
	}	
    }
    
    @Override
    public void disconnect() throws BackendException, ProxyException {
	try {
	    connection_.disconnect();
	} catch (LDAPException e) {
	    logger_.error("Cannot disconnect from LDAP server", e);
	    throw new BackendConnectException();
	}
    }

    @Override
    public boolean isAlive() throws BackendConnectException, BackendException,
	    AuthenticationException {
	return connection_.isConnectionAlive();
    }

    @Override
    public boolean isConnected() throws ProxyException {
	return connection_.isConnected();
    }

    @Override
    public AuthenticationInformation authenticate(
	    AuthenticationCredentials authenticationCredentials)
	    throws UserNotFoundException, ProxyException, LockedUserException,
	    AuthenticationException, BackendException {
	// create temporary connection for authentication...
	LDAPConnection tmpConnection = new LDAPConnection();
	connect_(tmpConnection);
	try {
	    String bindDN = ldapConfig_.getUIDAttr() + "=" + authenticationCredentials.getUserID().getUsername() + "," + ldapConfig_.getUserBase() +"," + ldapConfig_.getBaseDN();
	    logger_.debug("Trying to bind " + bindDN);
	    connection_.bind(LDAPConnection.LDAP_V3, bindDN, authenticationCredentials.getPassword());
	    SimpleAuthenticationInformation ret = new SimpleAuthenticationInformation();
	    ret.setAuthToken(generateAuthToken(authenticationCredentials.getUserID()));
	    // TODO: get this from somewhere
	    ret.setLastLogin(new Date()); 
	    ret.setUserID(authenticationCredentials.getUserID());
	    return ret;
	    
	} catch(LDAPException e) {
	    if (e.getResultCode() == 49) {
		throw new AuthenticationException("Invalid credentials...");
	    }else if (e.getResultCode() == 53) {
		throw new LockedUserException(e.getLDAPErrorMessage());
	    } else if (e.getResultCode() == 32) {
		throw new UserNotFoundException();
	    }
	    try {
		tmpConnection.disconnect();
	    } catch (LDAPException e1) {
		// very safe to ignore...
	    }
	    logger_.error("Couldn't bind to LDAP Server...", e);
	    throw new BackendConnectException();	    
	}
    }

    private AuthToken generateAuthToken(UserID userID) {
	// TODO read the default time to live for authTokens in memory
	AuthToken authToken = new SimpleAuthToken(this, randomGenerator_
	    .newUniqueLongNumber(), Timer.ONE_MINUTE);
	tokensCache_.put(authToken, userID);
	return authToken;

    }
    
    @Override
    public boolean isAuthTokenValid(AuthToken authToken) throws ProxyException {
	return tokensCache_.containsKey(authToken);
    }

    @Override
    public boolean isUsernameExist(String username)
	    throws AuthenticationException, BackendConnectException {
	try {
	    String searchBase = ldapConfig_.getUserBase() + "," + ldapConfig_.getBaseDN();
	    String searchFilter = ldapConfig_.getUIDAttr() + "=" + username;
	    logger_.debug("Trying to search in base '"+ searchBase + "' for '"+ searchFilter+"'");
	    		
	    LDAPSearchResults res = connection_.search(searchBase, LDAPConnection.SCOPE_SUB, searchFilter, new String[]{ldapConfig_.getUIDAttr()}, false);
	    if (res != null && res.getCount() > 0) {
		LDAPEntry entry = res.next();
		LDAPAttribute attr = entry.getAttribute(ldapConfig_.getUIDAttr());
		// sanity check
		if (attr.getStringValue().equalsIgnoreCase(username)) {
		    return true;
		}
	    }
	}catch(LDAPException e) {
	    logger_.error("Couldn't search in LDAP for username '" +username+"'", e);
	    throw new BackendException("Couldn't search in LDAP for username '" +username+"'");
	}
	return false;
    }

    @Override
    public void logout(AuthToken userAuthToken) throws ProxyException {
	tokensCache_.remove(userAuthToken);
    }

    @Override
    public void updateAuthenticationInformation(AuthToken userAuthToken,
	    AuthenticationCredentials newAuthenticationCredentials)
	    throws ProxyException, BackendConnectException,
	    UserNotFoundException {
	// TODO Auto-generated method stub

    }

    @Override
    public void lockAccount(AuthenticationCredentials authenticationCredentials)
	    throws ProxyException, BackendConnectException,
	    UserNotFoundException {
	throw new CATException("Not Implemented");

    }

    @Override
    public void unlockAccount(
	    AuthenticationCredentials authenticationCredentials)
	    throws ProxyException, BackendConnectException,
	    UserNotFoundException {
	throw new CATException("Not Implemented");

    }

    @Override
    public void updateAuthenticationInformation(UserID userID,
	    AuthenticationCredentials newAuthenticationCredentials)
	    throws ProxyException, UserNotFoundException,
	    BackendConnectException {
	throw new CATException("Not Implemented");

    }

}
